package org.beynet.docs.sync;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;

import javax.security.auth.Subject;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.log4j.Logger;
import org.beynet.docs.auth.GSyncPrincipal;
import org.beynet.docs.exceptions.GDocsServerError;
import org.beynet.docs.exceptions.UnexpectedStatusCode;
import org.beynet.docs.format.*;
import org.beynet.docs.http.GDocsHTTPConstants;
import org.beynet.docs.http.HttpResumableUploader;
import org.beynet.docs.http.HttpUploaderListener;
import org.beynet.docs.options.CommandLineOptions;
import org.beynet.docs.session.Session;
import org.beynet.docs.session.SessionFactory;

/**
 * this class is handling the synchronization task between a local file system and google docs servers
 * @author beynet
 *
 */
public class FileSync implements PrivilegedAction<Object> {

    /**
     * start to resume uploads
     * @param options
     * @param executor
     */
    public FileSync(CommandLineOptions options, ExecutorService executor,HttpUploaderListener listener) {
        this.resume = true;
        this.listener = listener;
        this.executor = executor;
        this.isRootSync = true;
        this.options = options;
    }

    public FileSync(boolean rootSync, List<FileToSync> sources, HttpUploaderListener listener, String folderPathDestination, CommandLineOptions options, ExecutorService executor) {
        this.resume = false;
        this.folderPathDestination = folderPathDestination;
        this.options = options;
        this.isRootSync = rootSync;
        this.executor = executor;
        this.sources = sources;
        this.listener = listener;
    }
    

    

    /**
     * compute URL of the target directory on the remote side
     * @return
     * @throws IOException
     * @throws ServiceException
     */
    protected URI getFolderDestinationURI() throws IOException, GDocsServerError,UnexpectedStatusCode {
        URI directPath = SessionFactory.getCurrentSession().getDirectoryByPath(getFolderDestinationPath());
        if (directPath==null) {
            logger.error("Could not retrieve URI for "+getFolderDestinationPath());
            throw new IOException("Could not retrieve URI for "+getFolderDestinationPath());
        }
        return (directPath);
    }

    /**
     * list files already on the remote side - gather some informations about the directory
     * @return a map containing :<br/>
     * RESUMABLE_REL_LINK => the URI to be used to create a resumable upload into this directory
     * FILES_CONTENT      => List&lt;RemoteFile&gt; : the files in the target directory
     * @throws IOException
     * @throws GDocsServerError
     */
    protected Map<String, Object> getFolderDestinationInformations() throws IOException, GDocsServerError,UnexpectedStatusCode {
        Map<String, Object> result = new HashMap<String, Object>();
        List<RemoteFile> remotes = new ArrayList<RemoteFile>();
        URI targetFolder = getFolderDestinationURI();


        // Get all documents in the target folder
        DocumentListFeed allEntries = new DocumentListFeed();
        DocumentListFeed tempFeed = SessionFactory.getCurrentSession().getFeed(targetFolder);

        // store links interesting for us
        for (Link l : tempFeed.getLinks()) {
            if (RESUMABLE_REL_LINK.equals(l.getRel())) {
                result.put(RESUMABLE_REL_LINK, l.getURI());
            }
        }
        do {
            allEntries.getEntries().addAll(tempFeed.getEntries());
            Link nextLink = tempFeed.getNextLink();
            if ((nextLink == null) || (tempFeed.getEntries().isEmpty())) {
                break;
            }
            tempFeed = SessionFactory.getCurrentSession().getFeed(nextLink.getURI());
        } while (true);

        // construct file list to sync
        // ---------------------------
        for (DocumentListEntry entry : allEntries.getEntries()) {
            if (DocumentType.FOLDER.equals(entry.getType())) {
                logger.debug("Found a directory into target :" + entry.getTitle());
                remotes.add(new RemoteFile(entry.getTitle(), folderPathDestination, null, true, entry));
            } else {
                logger.debug("file in current folder:" + entry.getTitle() + " " + entry.getSelfLink().getURI());
                remotes.add(new RemoteFile(entry.getTitle(), folderPathDestination, entry.getMd5Checksum(), false, entry));
            }
        }
        result.put(FILES_CONTENT, remotes);
        return (result);
    }

    /**
     * check if target directory exist, create it if necessary
     * @throws IOException
     * @throws GDocsServerError 
     * @throws UnexpectedStatusCode 
     * @throws ServiceException
     */
    protected void checkFolderDestination() throws IOException, URISyntaxException, GDocsServerError, UnexpectedStatusCode {
        URI destination = SessionFactory.getCurrentSession().getDirectoryByPath(getFolderDestinationPath());
        if (destination == null) {
            logger.info("creating target directory " + getFolderDestinationPath());
            String dest = getFolderDestinationPath();
            StringTokenizer tokens = new StringTokenizer(dest, "/");
            String done = null;
            while (tokens.hasMoreTokens()) {
                String current = tokens.nextToken();
                String previous = null;
                if (done == null) {
                    done = current;
                } else {
                    previous = done;
                    done = done + "/" + current;
                }
                if (SessionFactory.getCurrentSession().getDirectoryByPath(done) == null) {
                    URI destinationFolder = (previous == null)
                            ? new URI(GDocsHTTPConstants.LIST_MAIN)
                            : new URI(SessionFactory.getCurrentSession().getDirectoryByPath(previous).toString() + "/contents");
                    SessionFactory.getCurrentSession().createEmptyFolder(current, destinationFolder);
                }
            }
        }
    }

    /**
     * start the sync process
     */
    @Override
    public Object run() {
        try {
            List<Collection> colsAtRootLevel = SessionFactory.getCurrentSession().getCollectionsAtRootLevel();
            List<Collection> childs = colsAtRootLevel.get(0).getChilds();
            int size = childs.size();
        } catch (Exception ex) {
            java.util.logging.Logger.getLogger(FileSync.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        // are we resuming an upload already started
        // -----------------------------------------
        if (resume == true) {
            SyncStatus status = SyncStatus.getInstance();
            for (CurrentUpload cur : status.getUploading()) {
                FileToSync f = new FileToSync(new File(cur.getFilePath()));
                listener.addFileToObserver(f);
                f.addObserver(listener);
                HttpResumableUploader up = new HttpResumableUploader(f, cur.getURI(), SessionFactory.getCurrentSession(), options.getHttpChunkSize(), true);
                executor.submit(up);
            }

        }
        // standard case : uploading the provided source
        // ---------------------------------------------
        else {
            try {
                checkFolderDestination();
                Map<String, Object> remoteDirectoryInformation = getFolderDestinationInformations();
                processSync(sources, remoteDirectoryInformation);
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
        return (null);

    }

    /**
     * do the sync :<ul>
     * <li>first iter on local files to copy on the remote side</li>
     * <li>remove remaining files on remote files if delete option was provided</li>
     * </ul>
     * @param filesToSync
     * @param remoteFiles
     * @throws IOException
     * @throws GDocsServerError 
     * @throws UnexpectedStatusCode 
     * @throws ServiceException
     */
    private void processSync(List<FileToSync> filesToSync, Map<String, Object> folderDestinationInformations) throws IOException, GDocsServerError, UnexpectedStatusCode {
        @SuppressWarnings("unchecked")
        List<RemoteFile> remoteFiles = (List<RemoteFile>) folderDestinationInformations.get(FILES_CONTENT);
        for (FileToSync fileToSync : filesToSync) {
            if (Thread.currentThread().isInterrupted()) break;
            
            String md5 = null;
            File file = fileToSync.getFile();
            // do not compute md5 for directory
            // --------------------------------
            if (!file.isDirectory()) {
                InputStream is = new FileInputStream(file);
                try {
                    md5 = DigestUtils.md5Hex(is);
                    logger.debug("File : " + file.getName() + " md5=" + md5);
                } finally {
                    is.close();
                }
            }
            // we check if the file to upload already exist on the servers
            // ------------------------------------------------------------
            boolean remoteFound = false;
            for (RemoteFile remote : remoteFiles) {
                // the file is found - we compare the md5 signature
                // ------------------------------------------------
                if (file.getName().equals(remote.getName())) {
                    remoteFound = true;
                    if (!file.isDirectory()) {
                        logger.debug("remote file found md5=" + remote.getMD5Sum());
                        // comparing md5 - 
                        if (!md5.equals(remote.getMD5Sum())) {
                            logger.info("updating file :" + file.getPath());
                            FileUploaderHelper.updateFile(SessionFactory.getCurrentSession(), fileToSync, remote.getEntry(), getFolderDestinationURI(), (URI) folderDestinationInformations.get(RESUMABLE_REL_LINK), executor, options.getHttpChunkSize());
                        } else {
                            logger.info("md5 matching for file :" + file.getPath() + " no need to sync");
                            fileToSync.noChange();
                        }
                    }
                    remoteFiles.remove(remote);
                    break;
                }
            }
            // the file to upload was not found
            // --------------------------------
            if (remoteFound == false) {
                if (file.isDirectory()) {
                    // create empty directory
                    SessionFactory.getCurrentSession().createEmptyFolder(file.getName(), getFolderDestinationURI());
                } else {
                    // uploading file
                    FileUploaderHelper.uploadFile(SessionFactory.getCurrentSession(), fileToSync, getFolderDestinationURI(), (URI) folderDestinationInformations.get(RESUMABLE_REL_LINK), executor, options.getHttpChunkSize());
                }
            }

            // if file is a directory we make a recursion
            // ------------------------------------------
            if (file.isDirectory()) {
                List<FileToSync> toSync = fileToSync.returnChildsList();
                if (!toSync.isEmpty()) {
                    for (FileToSync f : toSync) {
                        listener.addFileToObserver(f);
                        f.addObserver(listener);
                    }
                    FileSync sync = new FileSync(false, toSync, listener, getFolderDestinationPath() + "/" + file.getName(), options, executor);
                    sync.run();
                } else {
                    fileToSync.endUpload();
                }

            }
        }

        // removing remote files not found on the local side if delete options was provided
        // --------------------------------------------------------------------------------
        if (options.deleteRemoved() == true) {

            for (RemoteFile remote : remoteFiles) {
                logger.info("Deleting from remote side file not found on local side:" + remote.getName());
                try {
                    remote.getEntry().delete(SessionFactory.getCurrentSession());
                } catch (Exception e) {
                    //retry once
                    remote.getEntry().delete(SessionFactory.getCurrentSession());
                }
            }
        }
    }

    public String getFolderDestinationPath() {
        return folderPathDestination;
    }
    private boolean isRootSync;
    private boolean resume;
    private List<FileToSync> sources;
    private String folderPathDestination;
    private CommandLineOptions options;
    private ExecutorService executor;
    private HttpUploaderListener listener;
    private Session session;
    private static final String RESUMABLE_REL_LINK = "http://schemas.google.com/g/2005#resumable-create-media";
    @SuppressWarnings("unused")
    private static final String RESUMABLE_UPDATE_REL_LINK = "http://schemas.google.com/g/2005#resumable-edit-media";
    private static final String FILES_CONTENT = "CONTENT";
    private final static Logger logger = Logger.getLogger(FileSync.class);
}